/********************************************************************
# Copyright 2018-2019 Daniel 'grindhold' Brendle
#
# This file is part of libphexfile.
#
# libphexfile is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later
# version.
#
# libphexfile is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with libphexfile.
# If not, see http://www.gnu.org/licenses/.
*********************************************************************/

namespace Phexfile {
    public errordomain PhexfileError {
        INVALID_FILE,
        ROOT_MUST_BE_OBJECT,
        INVALID_METHOD,
        INVALID_EXCITATION,
        WRONG_COORDINATE_LENGTH,
        WRONG_NUMBER_OF_ENERGY_RECORDS,
    }

    public struct Atom {
        public string atom;
        public double x;
        public double y;
        public double z;
    }

    public class Simulation : GLib.Object {
        public string title {get; set; default="";}
        public string description {get; set; default="";}
        public string begin {get; set; default="";}
        public string end {get; set; default="";}

        //private size_t energy_pad_length = 0;

        private List<Coordinate> coordinates;
        private List<string> excitations;
        private List<string> methods;
        private HashTable<string,string> errors;

        private size_t matrix_size = 0;
        private size_t geometry_size = 0;
        private size_t block_size = 0;
        private char* energies;

        protected Simulation() {
            this.coordinates = new List<Coordinate>();
            this.errors = new HashTable<string,string>(str_hash, str_equal);
        }

        public static Simulation load_from_binary(string filename) {
            //TODO: implement
            return new Simulation();
        }

        public static Simulation load_from_json_file(string filename) throws PhexfileError {
            var p = new Json.Parser();
            try {
                p.load_from_file(filename);
            } catch (GLib.Error e) {
                throw new PhexfileError.INVALID_FILE(e.message);
            }
            return Simulation.load_json(p);
        }

        public static Simulation load_from_json(string data) throws PhexfileError {
            var p = new Json.Parser();
            try {
                p.load_from_data(data);
            } catch (GLib.Error e) {
                throw new PhexfileError.INVALID_FILE(e.message);
            }
            return Simulation.load_json(p);
        }

        private static Simulation load_json(Json.Parser p) throws PhexfileError {
            var sim = new Simulation();

            var jsim_node = p.get_root();
            if (jsim_node.get_node_type() != Json.NodeType.OBJECT) {
                throw new PhexfileError.ROOT_MUST_BE_OBJECT("The root of a simulation must be a json object");
            }
            var jsim = jsim_node.get_object();
            sim.title = jsim.get_string_member("title");
            sim.description = jsim.get_string_member("description");
            sim.begin = jsim.get_string_member("begin");
            sim.end = jsim.get_string_member("end");

            jsim.get_array_member("coordinates").foreach_element((a,i,e)=>{
                var c = e.get_object();
                var coord = new Coordinate();
                coord.name = c.get_string_member("name");
                coord.steps = c.get_int_member("steps");
                coord.step_width = c.get_double_member("step_width");
                coord.from = c.get_int_member("from");
                coord.unit = c.get_string_member("unit");
                coord.fac = c.get_double_member("fac");
                sim.coordinates.append(coord);
            });

            jsim.get_array_member("excitations").foreach_element((a,i,e)=>{
                sim.excitations.append(e.get_string());

            });

            jsim.get_array_member("methods").foreach_element((a,i,e)=>{
                sim.methods.append(e.get_string());
            });

            sim.parse_energies(jsim.get_array_member("energies"));

            if (jsim.has_member("errors"))
                sim.parse_errors(jsim.get_object_member("errors"));
            
            return sim;
        }

        private void parse_errors(Json.Object errors) {
            errors.foreach_member((a, i, e)=> {
                this.errors.set(i,errors.get_string_member(i));
            });
        }

        public string get_error_at(int64[] coordinates) {
            string key = "(";
            for (int i = 0; i < coordinates.length; i++) {
                key += "%lld".printf(coordinates[i]);
                if (i < coordinates.length-1) {
                    key += ", ";
                }
            }
            key += ")";

            if (this.errors.contains(key))
                return this.errors.@get(key);
            else
                return "";
        }

        private void parse_energies(Json.Array energies) throws PhexfileError {

            energies.foreach_element((a,i,en)=>{
                var e = en.get_array();
                var s = e.get_string_element(e.get_length()-1);
                this.geometry_size = int.max(s.length, (int)this.geometry_size);
            });

            // TODO: this code might explode when the comment field of
            //       any of the XYZ-files contains non-ASCII characters
            //       fuck non-ASCII characters.

            size_t sod = sizeof(double);
            this.matrix_size = this.excitations.length() * this.methods.length() * sod;
            this.block_size = this.matrix_size + this.geometry_size;
            size_t energy_size = energies.get_length() * block_size;

            this.energies = (char*)GLib.malloc(energy_size);

            uint parsed_units = 0;
            energies.foreach_element((a, i, en)=>{
                var e = en.get_array();
                var matrix = e.get_array_element(this.coordinates.length());
                matrix.foreach_element((ao, io, eo)=>{
                    eo.get_array().foreach_element((ai, ii, ei)=> {
                        double energy_val = 0.0d;
                        if (ei.is_null())
                            energy_val = double.NAN;
                        else
                            energy_val = ei.get_double();
                        GLib.Memory.copy(
                            this.energies + (i * block_size) + (io * ai.get_length() * sod) + (ii * sod),
                            &energy_val,
                            sod
                        );
                    });
                });
                var s = e.get_string_element(e.get_length()-1);
                GLib.Memory.copy(
                    this.energies + (i * block_size) + matrix_size,
                    s,
                    geometry_size
                );
                parsed_units++;
            });

            uint expected_units = 1;
            foreach (var c in this.coordinates)
                expected_units *= (uint)c.steps;
            if (expected_units != parsed_units)
                throw new PhexfileError.WRONG_NUMBER_OF_ENERGY_RECORDS(
                    "Expected %u energy records, got %u energy records".printf(expected_units,parsed_units)
                );
        }

        public unowned List<Coordinate> get_coordinates() {
            return this.coordinates;
        }

        public unowned List<string> get_excitations() {
            return this.excitations;
        }

        public unowned List<string> get_methods() {
            return this.methods;
        }

        /**
         * Used to express which entry is the X axis in get_energies*-methods
         */
        public const int64 X_AXIS = -1;
        /**
         * Used to express which entry is the Y axis in get_energies*-methods
         */
        public const int64 Y_AXIS = -2;


        private int get_method_pos(string method) throws PhexfileError {
            var idx = 0;
            foreach (var m in this.methods) {
                if (method == m) {
                    return idx;
                }
                idx++;
            }
            throw new PhexfileError.INVALID_METHOD("No such method %s", method);
        }

        private int get_excitation_pos(string exc) throws PhexfileError {
            var idx = 0;
            foreach (var e in this.excitations) {
                if (exc == e) {
                    return idx;
                }
                idx++;
            }
            throw new PhexfileError.INVALID_EXCITATION("No such excitation %s", exc);
        }

        private int64 get_offset(int64[] coordinates, int method_pos=0, int exc_pos=0) {
            var coords = this.coordinates.copy();
            coords.reverse();
            int64 offset = 0;
            int idx = 0;
            int64 multiplier = 1;

            foreach (var c in coords) {
                int64 val = coordinates[coordinates.length - 1 - idx++];
                offset += multiplier * val * block_size;
                multiplier *= c.steps;
            }

            offset += method_pos * this.excitations.length() * sizeof(double);
            offset += exc_pos * sizeof(double);
            return offset;
        }

        public double get_energy_fixed(int64[] coordinates, string method, string exc) throws PhexfileError{
            if (coordinates.length != this.coordinates.length())
                throw new PhexfileError.WRONG_COORDINATE_LENGTH("The provided coordinate array has the wrong size. Should be %u, was %d", this.coordinates.length(), coordinates.length);
            int method_pos = this.get_method_pos(method);
            int exc_pos = this.get_excitation_pos(exc);
            double ret = 0.0;
            int64 offset = this.get_offset(coordinates, method_pos, exc_pos);
            GLib.Memory.copy(&ret, this.energies + offset, sizeof(double));
            return ret;
        }

        /**
         * Get the energies of the simulation at a specific method for specific
         * X- and Y- coordinates
         */
        public double[] get_energies(int64[] coordinates, string method) throws PhexfileError {
            if (coordinates.length != this.coordinates.length())
                throw new PhexfileError.WRONG_COORDINATE_LENGTH("The provided coordinate array has the wrong size. Should be %u, was %d", this.coordinates.length(), coordinates.length);

            int method_pos = this.get_method_pos(method);

            Coordinate coord_x = null;
            Coordinate coord_y = null;
            int coord_pos_x = -1;
            int coord_pos_y = -1;

            int idx = 0;
            foreach (var c in this.coordinates) {
                if (coordinates[coordinates.length - 1 - idx] == X_AXIS) {
                    coord_pos_x = coordinates.length - 1 - idx;
                    coord_x = c;
                }
                if (coordinates[coordinates.length - 1 - idx] == Y_AXIS) {
                    coord_pos_y = coordinates.length - 1 - idx;
                    coord_y = c;
                }
                idx++;
            }

            uint n_exc = this.excitations.length();
            char* workspace = (char*)(new double[coord_x.steps*coord_y.steps*n_exc]);

            int64 offset = 0;
            for (int64 x = coord_x.from; x < coord_x.steps; x++) {
                for (int64 y = coord_y.from; y < coord_y.steps; y++) {
                    coordinates[coord_pos_x] = x;
                    coordinates[coord_pos_y] = y;
                    offset = this.get_offset(coordinates, method_pos);
                    double* ret_offset = workspace + (x * coord_y.steps * n_exc * sizeof(double) +
                                        y * n_exc * sizeof(double) );
                    GLib.Memory.copy(ret_offset, this.energies + offset,
                                     sizeof(double) * n_exc);

                }
            }

            var ret = new double[coord_x.steps*coord_y.steps*n_exc];
            GLib.Memory.copy(ret, workspace, (size_t)(coord_x.steps*coord_y.steps*n_exc*sizeof(double)));
            GLib.free(workspace);
            return ret;
        }

        /**
         * Returns the minimum energy in this simulation
         */
        public double get_minimum_energy() {
            int64 iterations = 1;
            foreach (var c in this.coordinates) {
                iterations *= c.steps;
            }

            double lowest = double.MAX;

            for (int i = 0; i < iterations; i++) {
                for (int m = 0; m < this.matrix_size / sizeof(double); m++ ) {
                    double energy = *(((double*)(this.energies+i*this.block_size))+m);
                    if (Math.isnan(energy) == 0 && energy < lowest) lowest = energy;
                }
            }
            return lowest;
        }

        public string get_geometry_xyz(int64[] coords) {
            int64 offset = this.get_offset(coords);
            char[] ret = new char[this.geometry_size];
            GLib.Memory.copy(ret, this.energies+offset+this.matrix_size, this.geometry_size);
            return (string)ret;
        }

        public Atom?[] get_geometry_struct(int64[] coords) {
            var geo = this.get_geometry_xyz(coords);
            uint cnt = 0;

            var lines = geo.split("\n");
            var ret = new Array<Atom?>();

            foreach (var line in lines) {
                if (cnt++ < 2)
                    continue;
                if (line == "")
                    continue;
                var fields = /\s+/.split(line);
                var a = Atom();
                a.atom = fields[1];
                a.x = double.parse(fields[2]);
                a.y = double.parse(fields[3]);
                a.z = double.parse(fields[4]);
                ret.append_val(a);
            }

            return ret.data;
        }

        ~Simulation() {
            GLib.free(this.energies);
        }
    }

    public class Coordinate : GLib.Object {
        /**
         * Descriptive name of the Reaction coordinate
         */
        public string name {get; set; default="";}
        /**
         * Determines how many steps (PES scan points) occur on this coordinate
         */
        public int64 steps {get; set; default=0;}
        /**
         * Determines the start-value for steps for this reaction coordinate
         */
        public int64 from {get; set; default=0;}
        /**
         * Determines how much we have to count up for each step
         */
        public double step_width {get; set; default=0.0f;}
        /**
         * A string determining the unit in which 
         */
        public string unit {get; set; default="";}
        /**
         * A conversion factor that has to be applied to convert steps into unit
         */
        public double fac {get; set; default=1.0;}
    }
}
